package viz.flutter.vutil
/*
 * Copyright (C) 2006 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import android.os.Build
import android.provider.DocumentsContract
import android.system.ErrnoException
import android.system.Os
import android.text.TextUtils
import android.util.Log
import android.webkit.MimeTypeMap
import androidx.annotation.RequiresApi
import java.io.*
import java.nio.charset.StandardCharsets
import java.util.*
import java.util.regex.Pattern
import java.util.zip.CRC32
import java.util.zip.CheckedInputStream


/**
 * Tools for managing files.  Not for public consumption.
 * @hide
 */
object FileUtilsCompatible {
    private const val TAG = "FileUtils"
    const val S_IRWXU = 448
    const val S_IRUSR = 256
    const val S_IWUSR = 128
    const val S_IXUSR = 64
    const val S_IRWXG = 56
    const val S_IRGRP = 32
    const val S_IWGRP = 16
    const val S_IXGRP = 8
    const val S_IRWXO = 7
    const val S_IROTH = 4
    const val S_IWOTH = 2
    const val S_IXOTH = 1

    /** Regular expression for safe filenames: no spaces or metacharacters  */
    private val SAFE_FILENAME_PATTERN = Pattern.compile("[\\w%+,./=_-]+")
    private val EMPTY = arrayOfNulls<File>(0)

    /**
     * Set owner and mode of of given [File].
     *
     * @param mode to apply through `chmod`
     * @param uid to apply through `chown`, or -1 to leave unchanged
     * @param gid to apply through `chown`, or -1 to leave unchanged
     * @return 0 on success, otherwise errno.
     */
    fun setPermissions(path: File, mode: Int, uid: Int, gid: Int): Int {
        return setPermissions(path.absolutePath, mode, uid, gid)
    }

    /**
     * Set owner and mode of of given path.
     *
     * @param mode to apply through `chmod`
     * @param uid to apply through `chown`, or -1 to leave unchanged
     * @param gid to apply through `chown`, or -1 to leave unchanged
     * @return 0 on success, otherwise errno.
     */
    fun setPermissions(path: String, mode: Int, uid: Int, gid: Int): Int {
        try {
            Os.chmod(path, mode)
        } catch (e: ErrnoException) {
            Log.w(TAG, "Failed to chmod($path): $e")
            return e.errno
        }
        if (uid >= 0 || gid >= 0) {
            try {
                Os.chown(path, uid, gid)
            } catch (e: ErrnoException) {
                Log.w(TAG, "Failed to chown($path): $e")
                return e.errno
            }
        }
        return 0
    }

    /**
     * Set owner and mode of of given [FileDescriptor].
     *
     * @param mode to apply through `chmod`
     * @param uid to apply through `chown`, or -1 to leave unchanged
     * @param gid to apply through `chown`, or -1 to leave unchanged
     * @return 0 on success, otherwise errno.
     */
    fun setPermissions(fd: FileDescriptor?, mode: Int, uid: Int, gid: Int): Int {
        try {
            Os.fchmod(fd, mode)
        } catch (e: ErrnoException) {
            Log.w(TAG, "Failed to fchmod(): $e")
            return e.errno
        }
        if (uid >= 0 || gid >= 0) {
            try {
                Os.fchown(fd, uid, gid)
            } catch (e: ErrnoException) {
                Log.w(TAG, "Failed to fchown(): $e")
                return e.errno
            }
        }
        return 0
    }

    /**
     * Return owning UID of given path, otherwise -1.
     */
    fun getUid(path: String?): Int {
        return try {
            Os.stat(path).st_uid
        } catch (e: ErrnoException) {
            -1
        }
    }

    /**
     * Perform an fsync on the given FileOutputStream.  The stream at this
     * point must be flushed but not yet closed.
     */
    fun sync(stream: FileOutputStream?): Boolean {
        try {
            stream?.fd?.sync()
            return true
        } catch (e: IOException) {
        }
        return false
    }

    // copy a file from srcFile to destFile, return true if succeed, return
    // false if fail
    fun copyFile(srcFile: File?, destFile: File): Boolean {
        var result = false
        result = try {
            val `in`: InputStream = FileInputStream(srcFile)
            try {
                copyToFile(`in`, destFile)
            } finally {
                `in`.close()
            }
        } catch (e: IOException) {
            false
        }
        return result
    }

    /**
     * Copy data from a source stream to destFile.
     * Return true if succeed, return false if failed.
     */
    fun copyToFile(inputStream: InputStream, destFile: File): Boolean {
        return try {
            if (destFile.exists()) {
                destFile.delete()
            }
            val out = FileOutputStream(destFile)
            try {
                val buffer = ByteArray(4096)
                var bytesRead: Int
                while (inputStream.read(buffer).also { bytesRead = it } >= 0) {
                    out.write(buffer, 0, bytesRead)
                }
            } finally {
                out.flush()
                try {
                    out.fd.sync()
                } catch (e: IOException) {
                }
                out.close()
            }
            true
        } catch (e: IOException) {
            false
        }
    }

    /**
     * Check if a filename is "safe" (no metacharacters or spaces).
     * @param file  The file to check
     */
    fun isFilenameSafe(file: File): Boolean {
        // Note, we check whether it matches what's known to be safe,
        // rather than what's known to be unsafe.  Non-ASCII, control
        // characters, etc. are all unsafe by default.
        return SAFE_FILENAME_PATTERN.matcher(file.path).matches()
    }

    /**
     * Read a text file into a String, optionally limiting the length.
     * @param file to read (will not seek, so things like /proc files are OK)
     * @param max length (positive for head, negative of tail, 0 for no limit)
     * @param ellipsis to add of the file was truncated (can be null)
     * @return the contents of the file, possibly truncated
     * @throws IOException if something goes wrong reading the file
     */
    @Throws(IOException::class)
    fun readTextFile(file: File, max: Int, ellipsis: String?): String {
        var max = max
        val input: InputStream = FileInputStream(file)
        // wrapping a BufferedInputStream around it because when reading /proc with unbuffered
        // input stream, bytes read not equal to buffer size is not necessarily the correct
        // indication for EOF; but it is true for BufferedInputStream due to its implementation.
        val bis = BufferedInputStream(input)
        return try {
            val size = file.length()
            if (max > 0 || size > 0 && max == 0) {  // "head" mode: read the first N bytes
                if (size > 0 && (max == 0 || size < max)) max = size.toInt()
                val data = ByteArray(max + 1)
                val length = bis.read(data)
                if (length <= 0) return ""
                if (length <= max) return String(data, 0, length)
                if (ellipsis == null) String(data, 0, max) else String(data, 0, max) + ellipsis
            } else if (max < 0) {  // "tail" mode: keep the last N
                var len: Int
                var rolled = false
                var last: ByteArray? = null
                var data: ByteArray? = null
                do {
                    if (last != null) rolled = true
                    val tmp = last
                    last = data
                    data = tmp
                    if (data == null) data = ByteArray(-max)
                    len = bis.read(data)
                } while (len == data!!.size)
                if (last == null && len <= 0) return ""
                if (last == null) return String(data, 0, len)
                if (len > 0) {
                    rolled = true
                    System.arraycopy(last, len, last, 0, last.size - len)
                    System.arraycopy(data, 0, last, last.size - len, len)
                }
                if (ellipsis == null || !rolled) String(last) else ellipsis + String(last)
            } else {  // "cat" mode: size unknown, read it all in streaming fashion
                val contents = ByteArrayOutputStream()
                var len: Int
                val data = ByteArray(1024)
                do {
                    len = bis.read(data)
                    if (len > 0) contents.write(data, 0, len)
                } while (len == data.size)
                contents.toString()
            }
        } finally {
            bis.close()
            input.close()
        }
    }

    /**
     * Writes string to file. Basically same as "echo -n $string > $filename"
     *
     * @param filename
     * @param string
     * @throws IOException
     */
    @Throws(IOException::class)
    fun stringToFile(filename: String?, string: String?) {
        val out = FileWriter(filename)
        try {
            out.write(string)
        } finally {
            out.close()
        }
    }

    /**
     * Computes the checksum of a file using the CRC32 checksum routine.
     * The value of the checksum is returned.
     *
     * @param file  the file to checksum, must not be null
     * @return the checksum value or an exception is thrown.
     */
    @Throws(FileNotFoundException::class, IOException::class)
    fun checksumCrc32(file: File?): Long {
        val checkSummer = CRC32()
        var cis: CheckedInputStream? = null
        return try {
            cis = CheckedInputStream(FileInputStream(file), checkSummer)
            val buf = ByteArray(128)
            while (cis.read(buf) >= 0) {
                // Just read for checksum to get calculated.
            }
            checkSummer.value
        } finally {
            if (cis != null) {
                try {
                    cis.close()
                } catch (e: IOException) {
                }
            }
        }
    }

    /**
     * Delete older files in a directory until only those matching the given
     * constraints remain.
     *
     * @param minCount Always keep at least this many files.
     * @param minAge Always keep files younger than this age.
     * @return if any files were deleted.
     */
    fun deleteOlderFiles(dir: File, minCount: Int, minAge: Long): Boolean {
        require(!(minCount < 0 || minAge < 0)) { "Constraints must be positive or 0" }
        val files = dir.listFiles() ?: return false

        // Sort with newest files first
        Arrays.sort(files) { lhs, rhs -> (rhs.lastModified() - lhs.lastModified()).toInt() }

        // Keep at least minCount files
        var deleted = false
        for (i in minCount until files.size) {
            val file = files[i]

            // Keep files newer than minAge
            val age = System.currentTimeMillis() - file.lastModified()
            if (age > minAge) {
                if (file.delete()) {
                    Log.d(TAG, "Deleted old file $file")
                    deleted = true
                }
            }
        }
        return deleted
    }

    /**
     * Test if a file lives under the given directory, either as a direct child
     * or a distant grandchild.
     *
     *
     * Both files *must* have been resolved using
     * [File.getCanonicalFile] to avoid symlink or path traversal
     * attacks.
     */
    fun contains(dirs: Array<File?>, file: File?): Boolean {
        for (dir in dirs) {
            if (contains(dir, file)) {
                return true
            }
        }
        return false
    }

    /**
     * Test if a file lives under the given directory, either as a direct child
     * or a distant grandchild.
     *
     *
     * Both files *must* have been resolved using
     * [File.getCanonicalFile] to avoid symlink or path traversal
     * attacks.
     */
    fun contains(dir: File?, file: File?): Boolean {
        if (dir == null || file == null) return false
        var dirPath = dir.absolutePath
        val filePath = file.absolutePath
        if (dirPath == filePath) {
            return true
        }
        if (!dirPath.endsWith("/")) {
            dirPath += "/"
        }
        return filePath.startsWith(dirPath)
    }

    fun deleteContents(dir: File): Boolean {
        val files = dir.listFiles()
        var success = true
        if (files != null) {
            for (file in files) {
                if (file.isDirectory) {
                    success = success and deleteContents(file)
                }
                if (!file.delete()) {
                    Log.w(TAG, "Failed to delete $file")
                    success = false
                }
            }
        }
        return success
    }

    private fun isValidExtFilenameChar(c: Char): Boolean {
        return when (c) {
            '\u0000', '/' -> false
            else -> true
        }
    }

    /**
     * Check if given filename is valid for an ext4 filesystem.
     */
    fun isValidExtFilename(name: String?): Boolean {
        return name != null && name == buildValidExtFilename(name)
    }

    /**
     * Mutate the given filename to make it valid for an ext4 filesystem,
     * replacing any invalid characters with "_".
     */
    fun buildValidExtFilename(name: String): String {
        if (TextUtils.isEmpty(name) || "." == name || ".." == name) {
            return "(invalid)"
        }
        val res = StringBuilder(name.length)
        for (i in 0 until name.length) {
            val c = name[i]
            if (isValidExtFilenameChar(c)) {
                res.append(c)
            } else {
                res.append('_')
            }
        }
        trimFilename(res, 255)
        return res.toString()
    }

    private fun isValidFatFilenameChar(c: Char): Boolean {
        return if (0x00 <= c.toInt() && c.toInt() <= 0x1f) {
            false
        } else when (c) {
            '"', '*', '/', ':', '<', '>', '?', '\\', '|', ' ' -> false
            else -> true
        }
    }

    /**
     * Check if given filename is valid for a FAT filesystem.
     */
    fun isValidFatFilename(name: String?): Boolean {
        return name != null && name == buildValidFatFilename(name)
    }

    /**
     * Mutate the given filename to make it valid for a FAT filesystem,
     * replacing any invalid characters with "_".
     */
    fun buildValidFatFilename(name: String): String {
        if (TextUtils.isEmpty(name) || "." == name || ".." == name) {
            return "(invalid)"
        }
        val res = StringBuilder(name.length)
        for (i in 0 until name.length) {
            val c = name[i]
            if (isValidFatFilenameChar(c)) {
                res.append(c)
            } else {
                res.append('_')
            }
        }
        // Even though vfat allows 255 UCS-2 chars, we might eventually write to
        // ext4 through a FUSE layer, so use that limit.
        trimFilename(res, 255)
        return res.toString()
    }

    fun trimFilename(str: String?, maxBytes: Int): String {
        val res = StringBuilder(str!!)
        trimFilename(res, maxBytes)
        return res.toString()
    }

    private fun trimFilename(res: StringBuilder, maxBytes: Int) {
        var maxBytes = maxBytes
        var raw = res.toString().toByteArray(StandardCharsets.UTF_8)
        if (raw.size > maxBytes) {
            maxBytes -= 3
            while (raw.size > maxBytes) {
                res.deleteCharAt(res.length / 2)
                raw = res.toString().toByteArray(StandardCharsets.UTF_8)
            }
            res.insert(res.length / 2, "...")
        }
    }

    fun rewriteAfterRename(beforeDir: File?, afterDir: File?, path: String?): String? {
        if (path == null) return null
        val result = rewriteAfterRename(beforeDir, afterDir, File(path))
        return result?.absolutePath
    }

    fun rewriteAfterRename(beforeDir: File?, afterDir: File?, paths: Array<String?>?): Array<String?>? {
        if (paths == null) return null
        val result = arrayOfNulls<String>(paths.size)
        for (i in paths.indices) {
            result[i] = rewriteAfterRename(beforeDir, afterDir, paths[i])
        }
        return result
    }

    /**
     * Given a path under the "before" directory, rewrite it to live under the
     * "after" directory. For example, `/before/foo/bar.txt` would become
     * `/after/foo/bar.txt`.
     */
    fun rewriteAfterRename(beforeDir: File?, afterDir: File?, file: File?): File? {
        if (file == null || beforeDir == null || afterDir == null) return null
        if (contains(beforeDir, file)) {
            val splice = file.absolutePath.substring(
                    beforeDir.absolutePath.length)
            return File(afterDir, splice)
        }
        return null
    }

    /**
     * Generates a unique file name under the given parent directory. If the display name doesn't
     * have an extension that matches the requested MIME type, the default extension for that MIME
     * type is appended. If a file already exists, the name is appended with a numerical value to
     * make it unique.
     *
     * For example, the display name 'example' with 'text/plain' MIME might produce
     * 'example.txt' or 'example (1).txt', etc.
     *
     * @throws FileNotFoundException
     */
    @Throws(FileNotFoundException::class)
    fun buildUniqueFile(parent: File, mimeType: String, displayName: String): File {
        var name: String
        var ext: String?
        if (DocumentsContract.Document.MIME_TYPE_DIR == mimeType) {
            name = displayName
            ext = null
        } else {
            var mimeTypeFromExt: String?

            // Extract requested extension from display name
            val lastDot = displayName.lastIndexOf('.')
            if (lastDot >= 0) {
                name = displayName.substring(0, lastDot)
                ext = displayName.substring(lastDot + 1)
                mimeTypeFromExt = MimeTypeMap.getSingleton().getMimeTypeFromExtension(
                        ext.toLowerCase())
            } else {
                name = displayName
                ext = null
                mimeTypeFromExt = null
            }
            if (mimeTypeFromExt == null) {
                mimeTypeFromExt = "application/octet-stream"
            }
            val extFromMimeType = MimeTypeMap.getSingleton().getExtensionFromMimeType(
                    mimeType)
            if (mimeType == mimeTypeFromExt || ext == extFromMimeType) {
                // Extension maps back to requested MIME type; allow it
            } else {
                // No match; insist that create file matches requested MIME
                name = displayName
                ext = extFromMimeType
            }
        }
        var file = buildFile(parent, name, ext)

        // If conflicting file, try adding counter suffix
        var n = 0
        while (file.exists()) {
            if (n++ >= 32) {
                throw FileNotFoundException("Failed to create unique file")
            }
            file = buildFile(parent, "$name ($n)", ext)
        }
        return file
    }

    private fun buildFile(parent: File, name: String, ext: String?): File {
        return if (TextUtils.isEmpty(ext)) {
            File(parent, name)
        } else {
            File(parent, "$name.$ext")
        }
    }

    fun listFilesOrEmpty(dir: File): Array<File?> {
        val res = dir.listFiles()
        return res ?: EMPTY
    }
}
